<template>
  <form class="cmp cmp-hide row">
    <div class="col-4">
      <fieldset>
        <legend>参数设置</legend>
        <div class="fieldset-content">
          <div class="form-group form-item-row" align="center">
            <div class="form-item-label">主图</div>
            <div class="form-item-input"><input class="form-control" type="text" v-model="mainImagePath" placeholder="请选择主图" readonly /></div>
            <div class="form-item-suffix"><x-button @click="setMainImagePath">选择...</x-button></div>
          </div>
          <div class="form-group form-item-row" align="center">
            <div class="form-item-label">隐藏类型</div>
            <div class="form-item-input">
              <select class="form-control" v-model="hideType">
                <option v-for="(v,i) in hideTypeList" :key="i" :value="v.value">{{v.label}}</option>
              </select>
            </div>
          </div>
          <div class="form-group form-item-row" align="center" v-if="hideType==='image'">
            <div class="form-item-label">隐藏图片</div>
            <div class="form-item-input"><input class="form-control" type="text" v-model="hideImagePath" placeholder="请选择要隐藏的图片" readonly /></div>
            <div class="form-item-suffix"><x-button @click="setHideImagePath">选择...</x-button></div>
          </div>
          <div class="form-group form-item-row" align="center" v-else-if="hideType==='text'">
            <div class="form-item-label">隐藏文本</div>
            <div class="form-item-input"><textarea class="form-control" v-model.trim="hideText" placeholder="请选择要隐藏的文本"></textarea></div>
          </div>
          <div class="form-group form-item-row" align="center">
            <div class="form-item-label">阈值</div>
            <div class="form-item-input">
              <input class="form-control" type="number" :min="1" :step="1" :max="7" v-model.trim="threshold" placeholder="请输入阈值" />
            </div>
          </div>
          <div class="form-group form-item-row" align="center">
            <div class="form-item-label">导出位置</div>
            <div class="form-item-input"><input class="form-control" type="text" v-model="saveFilePath" placeholder="请选择文件处理后的存储位置" /></div>
            <div class="form-item-suffix"><x-button @click="setSaveFilePath">选择...</x-button></div>
          </div>
          <div class="form-group form-group-main"></div>
          <div class="form-group form-item-row" align="center">
            <x-button type="primary" block @click="encrypt" :loading="loadingCount > 0">开始处理</x-button>
          </div>
        </div>
      </fieldset>
    </div>
    <div class="col-8">
      <fieldset>
        <legend>结果预览</legend>
        <div class="fieldset-content">
          <div class="form-group form-group-main">
            <img :src="resultImageUrl" />
          </div>
          <div class="form-group form-item-row" align="center">
            <x-button type="primary" block @click="exportFile">导出</x-button>
          </div>
        </div>
      </fieldset>
    </div>
  </form>
</template>

<script>
const fs = global.require('fs')
const path = global.require('path')
const {remote} = global.require('electron')
const {dialog} = remote
const selfWindow= remote.getCurrentWindow()

const getImageDataUrl = (imagePath) => {
  return new Promise((resolve, reject) => {
    let paramType = (typeof imagePath)
    let ext = (path.extname(imagePath) || '').substring(1) || 'png'
    if (!imagePath) { reject('图片地址不能为空') }
    else if (paramType === 'string') {
      fs.readFile(imagePath, {encoding: 'base64'}, (err, data) => {
        if (err) {reject('图片读取失败')}
        else if (!data) {reject('未找到有效图片数据')}
        else {
          resolve(`data:image/${ext};base64,${data}`)
        }
      })
    }
    else { reject('图片地址无效') }
  })
}
const num2binary = (num) => {
  if (isNaN(parseInt(num))) {
    return ''
  } else {
    return num.toString(2).padStart(8, '0')
  }
}
const str2binary = (str) => {
  let result = []
  for (let i = 0; i < str.length; i++) {
      let ascii = str.charCodeAt(i)
      let binary = ascii.toString(2).padStart(16, 0)
      result.push(binary)
  }
  result.push('嘂'.charCodeAt(0).toString(2).padStart(16, 0))
  return result.join('').split('')
}
const getGroupArr = (arr, num) => {
  const result = []
  while (arr.length > 0) {
    result.push(arr.splice(0, num))
  }
  return result
}
const hideDataInPixel = (arr, pixel, numPerPixel) => {
  return pixel.map(data => {
    const bData = num2binary(data)
    const hideData = arr.splice(0, numPerPixel)
    return parseInt(`${bData.slice(0, 8 - numPerPixel)}${hideData.join('')}`, 2)
  })
}
const hideBinaryInPixel = (textBinary, pixel, numPerPixel) => {
  return pixel.map(data => {
    const hideChar = textBinary.splice(0, numPerPixel).join('')
    if (hideChar) {
      const bData = num2binary(data)
      return parseInt(`${bData.slice(0, 8 - hideChar.length)}${hideChar}`, 2)
    }
    return data
  })
}

export default {
  name: 'hide',
  data () {
    return {
      mainImagePath: '',
      hideImagePath: '',
      hideText: '',
      mainImageDataUrl: '',
      hideImageDataUrl: '',
      saveFilePath: this.$sharedObject('desktopPath'),
      threshold: 2,
      resultCanvas: null,
      resultImageUrl: '',
      mainImageWidth: 0,
      mainImageHeight: 0,
      hideType: 'image',
      hideTypeList: [
        {value: 'image', label: '图片'},
        {value: 'text', label: '文本'},
      ],
      loadingCount: 0,
    }
  },
  methods: {
    // 选择主图
    setMainImagePath () {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择文件',
        buttonLabel:'确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openFile']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.mainImagePath = filePaths[0]
        this.loadingCount++
        getImageDataUrl(this.mainImagePath).then((dataUrl) => {
          this.mainImageDataUrl = dataUrl
        }).catch((err) => {
          this.$toast(err)
        }).finally(() => {
          this.loadingCount--
        })
      }
    },
    // 选择隐藏图片
    setHideImagePath () {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择文件',
        buttonLabel:'确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openFile']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.hideImagePath = filePaths[0]
        this.loadingCount++
        getImageDataUrl(this.hideImagePath).then((dataUrl) => {
          this.hideImageDataUrl = dataUrl
        }).catch((err) => {
          this.$toast(err)
        }).finally(() => {
          this.loadingCount--
        })
      }
    },
    // 设置解密文件保存位置
    setSaveFilePath () {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择目录',
        buttonLabel:'确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openDirectory']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.saveFilePath = filePaths[0]
      }
    },
    // 获取imgData
    getImgData (imgObj) {
      let canvasObj = document.createElement('canvas')
      canvasObj.width = imgObj.width
      canvasObj.height = imgObj.height
      let ctx = canvasObj.getContext('2d')
      ctx.drawImage(imgObj, 0, 0, canvasObj.width, canvasObj.height)
      let imgData = ctx.getImageData(0, 0, canvasObj.width, canvasObj.height)
      return imgData.data
    },
    // 表单校验
    checkForm () {
      let result = true
      if (!this.mainImagePath) {
        result = false
        this.$toast('主图不能为空')
      }
      if (this.hideType === 'image' && !this.hideImagePath) {
        result = false
        this.$toast('隐藏图片不能为空')
      }
      if (this.hideType === 'text' && !this.hideText) {
        result = false
        this.$toast('隐藏文本不能为空')
      }
      if (!this.threshold || this.threshold <= 0 || this.threshold >= 8) {
        result = false
        this.$toast('阈值应为1~7的数字')
      }
      return result
    },
    // 加密
    encrypt () {
      if (!this.checkForm()) return
      this.loadingCount++
      let taskList = [this.$jstools.loadImage(this.mainImageDataUrl)]
      if (this.hideType === 'image') {
        taskList.push(this.$jstools.loadImage(this.hideImageDataUrl))
      }
      Promise.all(taskList).then(([mainImage, hideObj]) => {
        if (!mainImage) { return this.$toast('主图加载失败') }
        this.mainImageWidth = mainImage.width
        this.mainImageHeight = mainImage.height
        let mainImageData = this.getImgData(mainImage)
        if (this.hideType === 'image') {
          if (!hideObj) { return this.$toast('隐藏图片加载失败') }
          let hideImageData = this.getImgData(hideObj)
          this.doHideImage(mainImageData, hideImageData)
        } else if (this.hideType === 'text') {
          this.doHideText(mainImageData, this.hideText)
        }
      }).finally(() => {
        this.loadingCount--
      })
    },
    // ImageData转base64
    imgData2dataUrl (inputImageData) {
      let canvasObj = document.createElement('canvas')
      canvasObj.width = this.mainImageWidth
      canvasObj.height = this.mainImageHeight
      let ctx = canvasObj.getContext('2d')
      let imageData = new ImageData(inputImageData, canvasObj.width, canvasObj.height)
      ctx.putImageData(imageData, 0, 0)
      this.resultCanvas = canvasObj
      this.resultImageUrl = canvasObj.toDataURL('image/png')
    },
    // 隐藏图片
    doHideImage (mainImageData, hideImageData) {
      const mainImageLength = mainImageData.length
      const hideImageLength = hideImageData.length
      if (hideImageLength * 8 > mainImageLength * this.threshold) {
        return this.$toast('主图过小不足以隐藏目标图片')
      }
      this.loadingCount++
      let tempArr = []
      let index = 0
      for (let i = 0; i < hideImageData.length; i += 4) {
        const hr = hideImageData[i]
        const hg = hideImageData[i + 1]
        const hb = hideImageData[i + 2]
        const ha = hideImageData[i + 3]
        const bins = [num2binary(hr), num2binary(hg), num2binary(hb), num2binary(ha)].join('').split('')
        const countPerGroup = 4 * this.threshold
        tempArr = tempArr.concat(bins)
        const count = Math.floor(tempArr.length / countPerGroup)
        const arrForGroup = tempArr.splice(0, count * countPerGroup)
        const bins_2 = getGroupArr(arrForGroup, countPerGroup)
        bins_2.forEach(arr => {
          const mr = mainImageData[index]
          const mg = mainImageData[index + 1]
          const mb = mainImageData[index + 2]
          const ma = mainImageData[index + 3]
          const [r, g, b, a] = hideDataInPixel(arr, [mr, mg, mb, ma], this.threshold)
          mainImageData[index] = r
          mainImageData[index + 1] = g
          mainImageData[index + 2] = b
          mainImageData[index + 3] = a
          index += 4
        })
      }
      this.imgData2dataUrl(mainImageData)
      this.loadingCount--
    },
    // 隐藏文本
    doHideText (mainImageData, text) {
      this.loadingCount++
      const mainImageLength = mainImageData.length
      const textBinary = str2binary(text)
      if (textBinary.length > mainImageLength * this.threshold) {
        return this.$toast('主图过小不足以隐藏文本')
      }
      this.loadingCount++
      for (let i = 0; i < mainImageData.length; i += 4) {
        const mr = mainImageData[i]
        const mg = mainImageData[i + 1]
        const mb = mainImageData[i + 2]
        const ma = mainImageData[i + 3]
        const [r, g, b, a] = hideBinaryInPixel(textBinary, [mr, mg, mb, ma], this.threshold)
        mainImageData[i] = r
        mainImageData[i + 1] = g
        mainImageData[i + 2] = b
        mainImageData[i + 3] = a
        if (textBinary.length === 0) break
      }
      this.imgData2dataUrl(mainImageData)
      this.loadingCount--
    },
    // 导出
    exportFile () {
      if (!this.resultCanvas) {
        return this.$toast('无数据')
      }
      this.loadingCount++
      let fileName = `hide_${Date.now()}.png`
      let filePath = path.join(this.saveFilePath, fileName)
      this.resultCanvas.toBlob((blob) => {
        this.loadingCount--
        if (!blob) {return this.$toast('无数据')}
        this.loadingCount++
        blob.arrayBuffer().then((res) => {
          this.loadingCount++
          this.$sharedObject('jstoolsNode').writeFile(filePath, Buffer.from(res)).then(() => {
            this.$toast('导出完成')
          }).catch((err) => {
            this.$toast('导出错误：' + err)
          }).finally(() => {
            this.loadingCount--
          })
        }).catch((err) => {
          this.$toast('发生错误：' + err)
        }).finally(() => {
          this.loadingCount--
        })
      }, 'image/png')
    },
  }
}
</script>

<style scoped lang="less">
@import "~@/assets/css/_mixins.less";
@import "~@/assets/css/_color.less";
.form-item-row {
  display: flex;
  .form-item-label {
    min-width: 4em;
    margin-right: .5em;
    text-align-last: justify;
  }
  .form-item-input {
    width: 0;
    flex-grow: 1;
    text-align: left;
  }
  .form-item-suffix {
    margin-left: .5em;
    min-width: 2em;
  }
  &[align="center"] {
    align-items: center;
  }
}
.cmp-hide {
  height: 100%;
  fieldset {
    height: 100%;
    .fieldset-content {
      height: calc(100% - 2em);
      display: flex;
      flex-direction: column;
      .form-group-main {
        height: 0;
        flex-grow: 1;
        img {
          display: block;
          width: 100%;
          height: 100%;
          object-fit: contain;
          background-color: @bgGrey;
        }
      }
    }
  }
}
</style>
